Виктория:
1.Название проекта: Анализ пользовательского поведения в мобильном приложении
2.Описание проекта: Являясь аналитиком в стартапе, который продаёт продукты питания, необходимо разобраться, как ведут себя пользователи мобильного приложения, изучить воронку продаж, провести А/В-тесты и узнать, как пользователи доходят до покупки.
3.Описание данных: файл /datasets/logs_exp.csv
— EventName — название события;
— DeviceIDHash — уникальный идентификатор пользователя;
— EventTimestamp — время события;
— ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
import datetime as dt
from scipy import stats as st
from plotly import graph_objects as go
import math as mth
import seaborn as sns
#загружаю данные
try:
df=pd.read_csv('/datasets/logs_exp.csv', sep='\t')
except FileNotFoundError:
df=pd.read_csv('logs_exp.csv', sep='\t')
#исправляю названия колонок в нижний регистр в виде Snake Case
df.columns = ['event_name',
'user_id',
'event_timestamp',
'exp_id']
#проверяю датафрейм на дубликаты
print('Всего дубликатов в таблице:', df.duplicated().sum())
Всего дубликатов в таблице: 413
Виктория: Обнаружно 413 пропусков в таблице, которые нужно удалить, чтобы не искажать результаты исследования.
Виктория:
✅Добавила рассчёт доли дубликатов, который составил 0,17% от общей информации, что позволяет принять решение об удалении лишних строк.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 244126 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 244126 non-null object 1 user_id 244126 non-null int64 2 event_timestamp 244126 non-null int64 3 exp_id 244126 non-null int64 dtypes: int64(3), object(1) memory usage: 7.5+ MB
total_records = 244126
duplicate_records = 413
duplicate_ratio = duplicate_records / total_records
print(f"Доля дубликатов: {duplicate_ratio:.2%}")
Доля дубликатов: 0.17%
#удаляю явные дубликаты из таблицы
df = df.drop_duplicates()
#проверяю датафрейм на пропуски
print('Всего пропусков в таблице:\n', df.isna().sum())
Всего пропусков в таблице: event_name 0 user_id 0 event_timestamp 0 exp_id 0 dtype: int64
Виктория: Пропусков обнаружено не было.
df.info()
<class 'pandas.core.frame.DataFrame'> Int64Index: 243713 entries, 0 to 244125 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 event_name 243713 non-null object 1 user_id 243713 non-null int64 2 event_timestamp 243713 non-null int64 3 exp_id 243713 non-null int64 dtypes: int64(3), object(1) memory usage: 9.3+ MB
Виктория: В столбце времени события, 'event_timestamp', данные представлены в формате int64, которые нужно поменять на формат datetime.
#меняю тип данных
df['event_timestamp'] = pd.to_datetime(df['event_timestamp'], unit='s')
#добавля столбец даты и времени, а также отдельный столбец дат
df['event_date'] = df['event_timestamp'].dt.date
df['event_date'] = pd.to_datetime(df['event_date'])
df['event_datetime'] = pd.to_datetime(df['event_timestamp'], unit='s')
Виктория: В данные были добавлены столбец даты и времени, а также отдельный столбец дат.
Сколько всего событий в логе?
print('Всего событий в логе:', len(df.event_name))
Всего событий в логе: 243713
Сколько всего пользователей в логе?
print('Всего пользователей в логе:', len(df.user_id))
Всего пользователей в логе: 243713
Сколько в среднем событий приходится на пользователя?
data_mean = df.groupby('user_id', as_index=False).agg({'event_name':'count'})
print('Среднее количество событий на пользователя:', data_mean.event_name.median())
Среднее количество событий на пользователя: 20.0
Виктория: Для подсчёта среднего количества была использована медиана, чтобы не искажались результаты.
#проверяю наличия пользователей, попавших в две группы
group_246 = set(df[df['exp_id'] == 246]['user_id'])
group_247 = set(df[df['exp_id'] == 247]['user_id'])
group_248 = set(df[df['exp_id'] == 248]['user_id'])
duplicate_users = group_246.intersection(group_247, group_248)
#вывожу количества пользователей, попавших в две группы
print("Количество пользователей, попавших в обе группы:", len(duplicate_users))
Количество пользователей, попавших в обе группы: 0
Данными за какой период вы располагаете? Найдите максимальную и минимальную дату. Изучите, как меняется количество данных: постройте столбчатую диаграмму, которая отобразит количество событий в зависимости от времени в разрезе групп. Можно ли быть уверенным, что у вас одинаково полные данные за весь период? Технически в логи новых дней по некоторым пользователям могут «доезжать» события из прошлого — это может «перекашивать данные». Определите, с какого момента данные полные и отбросьте более старые. Данными за какой период времени вы располагаете на самом деле?
print('Минимальная дата в таблице:', df['event_date'].min())
Минимальная дата в таблице: 2019-07-25 00:00:00
print('Максимальная дата в таблице:', df['event_date'].max())
Максимальная дата в таблице: 2019-08-07 00:00:00
Виктория: В логах представлены даты за 2019 год с 25 июля по 7 августа.
plt.figure(figsize=(15,7))
plt.title('Распределение данных по дате и времени')
plt.xlabel('Даты')
plt.ylabel('Количество событий')
df['event_datetime'].hist(bins=50, color='IndianRed');
Виктория: Для получения полной картины принято решение использовать данные за период с 1 по 7 августа 2019 года, так как по графику видно, что они достаточно представительны.
#выбираю данные только за период с 1 по 7 августа 2019 год
filtered_df = df[(df['event_datetime'].dt.date >= pd.to_datetime('2019-08-01').date()) &
(df['event_datetime'].dt.date <= pd.to_datetime('2019-08-07').date())]
#вывожу результаты
filtered_df
| event_name | user_id | event_timestamp | exp_id | event_date | event_datetime | |
|---|---|---|---|---|---|---|
| 2828 | Tutorial | 3737462046622621720 | 2019-08-01 00:07:28 | 246 | 2019-08-01 | 2019-08-01 00:07:28 |
| 2829 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:00 | 246 | 2019-08-01 | 2019-08-01 00:08:00 |
| 2830 | MainScreenAppear | 3737462046622621720 | 2019-08-01 00:08:55 | 246 | 2019-08-01 | 2019-08-01 00:08:55 |
| 2831 | OffersScreenAppear | 3737462046622621720 | 2019-08-01 00:08:58 | 246 | 2019-08-01 | 2019-08-01 00:08:58 |
| 2832 | MainScreenAppear | 1433840883824088890 | 2019-08-01 00:08:59 | 247 | 2019-08-01 | 2019-08-01 00:08:59 |
| ... | ... | ... | ... | ... | ... | ... |
| 244121 | MainScreenAppear | 4599628364049201812 | 2019-08-07 21:12:25 | 247 | 2019-08-07 | 2019-08-07 21:12:25 |
| 244122 | MainScreenAppear | 5849806612437486590 | 2019-08-07 21:13:59 | 246 | 2019-08-07 | 2019-08-07 21:13:59 |
| 244123 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:43 | 246 | 2019-08-07 | 2019-08-07 21:14:43 |
| 244124 | MainScreenAppear | 5746969938801999050 | 2019-08-07 21:14:58 | 246 | 2019-08-07 | 2019-08-07 21:14:58 |
| 244125 | OffersScreenAppear | 5746969938801999050 | 2019-08-07 21:15:17 | 246 | 2019-08-07 | 2019-08-07 21:15:17 |
240887 rows × 6 columns
Много ли событий и пользователей вы потеряли, отбросив старые данные?
print('Количество всех пользователей:', df.user_id.nunique())
print('Количество пользователей с 1 по 7 августа 2019г.:', filtered_df.user_id.nunique())
percentage_change = (filtered_df.user_id.nunique() - df.user_id.nunique()) / df.user_id.nunique() * 100
print(f"Процентное изменение количества пользователей: {percentage_change:.2f}%")
Количество всех пользователей: 7551 Количество пользователей с 1 по 7 августа 2019г.: 7534 Процентное изменение количества пользователей: -0.23%
print('Количество всех событий:', df.event_name.count())
print('Количество событий с 1 по 7 августа 2019г.:', filtered_df.event_name.count())
percentage_change = (filtered_df.event_name.count() - df.event_name.count()) / df.event_name.count() * 100
print(f"Процентное изменение количества событий: {percentage_change:.2f}%")
Количество всех событий: 243713 Количество событий с 1 по 7 августа 2019г.: 240887 Процентное изменение количества событий: -1.16%
Виктория: После применения фильтрации данных мы обнаружили, что количество пользователей уменьшилось на 0,23%, а количество событий сократилось на 1,16%. Эти потери являются незначительными и несущественными для общего объема данных.
Проверьте, что у вас есть пользователи из всех трёх экспериментальных групп.
filtered_df.groupby('exp_id', as_index=False).agg({'user_id':'nunique'}).rename(columns={'user_id':'n_users'})
| exp_id | n_users | |
|---|---|---|
| 0 | 246 | 2484 |
| 1 | 247 | 2513 |
| 2 | 248 | 2537 |
Виктория: По таблице видно, что количество пользователей по группам распределено не равномерно. Принято решение пока оставить этот вопрос.
Посмотрите, какие события есть в логах, как часто они встречаются. Отсортируйте события по частоте.
filtered_df.event_name.value_counts()
MainScreenAppear 117328 OffersScreenAppear 46333 CartScreenAppear 42303 PaymentScreenSuccessful 33918 Tutorial 1005 Name: event_name, dtype: int64
#переношу данные по количеству событий
event_counts = {
'MainScreenAppear': 117328,
'OffersScreenAppear': 46333,
'CartScreenAppear': 42303,
'PaymentScreenSuccessful': 33918,
'Tutorial': 1005
}
#рисую гистограмму
plt.figure(figsize=(10,8))
plt.bar(event_counts.keys(), event_counts.values(), color='IndianRed')
plt.xlabel('Название события')
plt.ylabel('Количество раз')
plt.title('Частота событий в логах')
#поворачиваю надписи на оси х для лучшей визуальной составляющей
plt.xticks(rotation=45)
plt.show()
Виктория: По проведенному анализу данных, было обнаружено, что в логах присутствуют 5 различных типов событий.
Самым частым событием является взаимодействие с главным экраном (MainScreenAppear), которое было зафиксировано 117328 раз.
Затем идет продающий экран (OffersScreenAppear), который был замечен у 46333 пользователей.
Немного меньшее количество пользователей увидели экран корзины (CartScreenAppear) - 42303 раза.
Только 33918 раз событие успешной оплаты (PaymentScreenSuccessful) было зафиксировано в логах.
Наконец, мастер-классы (Tutorial) встречаются в событиях всего лишь 1005 раз.
Эти данные позволяют нам получить представление о том, как пользователи взаимодействуют с приложением и какие этапы процесса они проходят.
Посчитайте, сколько пользователей совершали каждое из этих событий. Отсортируйте события по числу пользователей. Посчитайте долю пользователей, которые хоть раз совершали событие.
users_per_event = filtered_df.groupby('event_name', as_index=False) \
.agg({'user_id':'nunique'}) \
.sort_values('user_id', ascending=False)
users_per_event.columns = ['event_name', 'users_per_event']
#считаю долю пользователей
users_per_event['user_share_%'] = round((users_per_event['users_per_event'] / filtered_df.user_id.nunique()) * 100, 2)
users_per_event
| event_name | users_per_event | user_share_% | |
|---|---|---|---|
| 1 | MainScreenAppear | 7419 | 98.47 |
| 2 | OffersScreenAppear | 4593 | 60.96 |
| 0 | CartScreenAppear | 3734 | 49.56 |
| 3 | PaymentScreenSuccessful | 3539 | 46.97 |
| 4 | Tutorial | 840 | 11.15 |
plt.figure(figsize=(12, 5))
plt.title('Количество пользователей, совершивших событие и их доля от общего числа пользователей')
sns.barplot(y=users_per_event['event_name'], x=users_per_event['users_per_event'], palette='Reds', orient='h')
plt.xlabel('Количество пользователей')
plt.ylabel('Название события')
plt.show()
Виктория: Анализ данных показывает, что главную страницу сайта посетили 7419 пользователей, что составляет 98.47% от общего числа пользователей. Это говорит о высокой популярности и привлекательности главной страницы.
Страницу товара просмотрели 4593 пользователей, что составляет 60.96% от общего числа пользователей. Это говорит о том, что большинство пользователей проявляют интерес к конкретным товарам и исследуют их подробнее.
Корзину просмотрели 3734 пользователя, что составляет 49.56% от общего числа пользователей. Это означает, что значительная часть пользователей проявляет намерение совершить покупку, добавляя товары в корзину.
Из всех пользователей, 3539 завершили оплату, что составляет 46.97% от общего числа пользователей. Это говорит о том, что некоторые пользователи, хотя и проявляют интерес к покупке, не доходят до финального этапа оплаты.
Урок просмотрели 840 пользователей, что составляет 11.15% от общего числа пользователей. Это может свидетельствовать о наличии дополнительного контента или обучающих материалов на сайте, которые привлекают некоторую аудиторию.
Предположите, в каком порядке происходят события. Все ли они выстраиваются в последовательную цепочку? Их не нужно учитывать при расчёте воронки.
Виктория: Для определения порядка происходящих событий можно воспользоваться информацией о полльзовательском опыте покупки в других сервисах, а также о количестве уникальных пользователей, которые выполнили каждое из событий.
Исходя из этой информации, можно сделать следующие предположения о предполагаемом порядке событий: Tutorial -> MainScreenAppear -> OffersScreenAppear -> CartScreenAppear -> PaymentScreenSuccessful.
Однако, стоит отметить, что данная последовательность не обязательно является строго линейной цепочкой, так как пользователи могут выполнять события в различных комбинациях или даже пропускать некоторые из них. Так, в событии Tutorial было явно меньше пользователей, чем в окочнательной покупке PaymentScreenSuccessful.
Таким образом, не все этапы являются обязательными для совершения покупки пользователем. То есть событие Tutorial вполне может оказаться необязательным к прохождению.
По воронке событий посчитайте, какая доля пользователей проходит на следующий шаг воронки (от числа пользователей на предыдущем). То есть для последовательности событий A → B → C посчитайте отношение числа пользователей с событием B к количеству пользователей с событием A, а также отношение числа пользователей с событием C к количеству пользователей с событием B.
#рассчитываю количества пользователей на каждом шаге воронки по группам
funnel_counts = filtered_df.groupby(['exp_id', 'event_name']).agg({'user_id': 'nunique'}).reset_index()
#удаляю события Tutorial из данных
funnel_counts = funnel_counts[funnel_counts['event_name'] != 'Tutorial']
funnel_counts
| exp_id | event_name | user_id | |
|---|---|---|---|
| 0 | 246 | CartScreenAppear | 1266 |
| 1 | 246 | MainScreenAppear | 2450 |
| 2 | 246 | OffersScreenAppear | 1542 |
| 3 | 246 | PaymentScreenSuccessful | 1200 |
| 5 | 247 | CartScreenAppear | 1238 |
| 6 | 247 | MainScreenAppear | 2476 |
| 7 | 247 | OffersScreenAppear | 1520 |
| 8 | 247 | PaymentScreenSuccessful | 1158 |
| 10 | 248 | CartScreenAppear | 1230 |
| 11 | 248 | MainScreenAppear | 2493 |
| 12 | 248 | OffersScreenAppear | 1531 |
| 13 | 248 | PaymentScreenSuccessful | 1181 |
#создаю словаря для определения порядка шагов воронки
step_order = {'MainScreenAppear': 1, 'OffersScreenAppear': 2, 'CartScreenAppear': 3, 'PaymentScreenSuccessful': 4}
#сортирую данных по порядку шагов воронки
funnel_counts['step_order'] = funnel_counts['event_name'].map(step_order)
funnel_counts.sort_values(['exp_id', 'step_order'], inplace=True)
#рассчитываю доли пользователей на каждом шаге воронки по группам
funnel_counts['conversion_rate'] = funnel_counts.groupby('exp_id')['user_id'].apply(lambda x: x / x.shift(1))
#разделяю таблицу по группам исследования
funnel_counts_246 = funnel_counts[funnel_counts['exp_id']==246]
funnel_counts_247 = funnel_counts[funnel_counts['exp_id']==247]
funnel_counts_248 = funnel_counts[funnel_counts['exp_id']==248]
print(funnel_counts_246, '\n', funnel_counts_247, '\n', funnel_counts_248)
exp_id event_name user_id step_order conversion_rate
1 246 MainScreenAppear 2450 1 NaN
2 246 OffersScreenAppear 1542 2 0.629388
0 246 CartScreenAppear 1266 3 0.821012
3 246 PaymentScreenSuccessful 1200 4 0.947867
exp_id event_name user_id step_order conversion_rate
6 247 MainScreenAppear 2476 1 NaN
7 247 OffersScreenAppear 1520 2 0.613893
5 247 CartScreenAppear 1238 3 0.814474
8 247 PaymentScreenSuccessful 1158 4 0.935380
exp_id event_name user_id step_order conversion_rate
11 248 MainScreenAppear 2493 1 NaN
12 248 OffersScreenAppear 1531 2 0.614120
10 248 CartScreenAppear 1230 3 0.803396
13 248 PaymentScreenSuccessful 1181 4 0.960163
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'Контрольная группа 246',
y=funnel_counts_246.event_name,
x=funnel_counts_246.user_id,
textinfo="value+percent previous",
marker=dict(color='CadetBlue')))
fig.add_trace(go.Funnel(
name='Контрольная группа 247',
y=funnel_counts_247.event_name,
x=funnel_counts_247.user_id,
textposition='inside',
textinfo='value+percent previous',
marker=dict(color='IndianRed')))
fig.add_trace(go.Funnel(
name='Экспериментальная группа 248',
y=funnel_counts_248.event_name,
x=funnel_counts_248.user_id,
textposition='inside',
textinfo='value+percent previous',
marker=dict(color='DarkSeaGreen')))
fig.update_layout(title='Доля пользователей по воронке событий')
fig.show()
На каком шаге теряете больше всего пользователей?
Виктория: Анализируя воронку событий, было обнаружено, что наибольшее количество пользователей (37% для группы 246 и 39% для групп 247 и 248) покидает нас после просмотра главного экрана (MainScreenAppear) и даже не переходит к каталогу товаров (OffersScreenAppear). Возможно у пользователей возникают технические неполадки с переходом просмотру продукции, либо происходит некорретное отображение основной страницы.
Какая доля пользователей доходит от первого события до оплаты?
Виктория: Менее половины пользователей доходит до этапа оплаты. Вначале 2450, 2476, 2493 пользователей, а в конце лишь 1200, 1158 и 1181, то есть
😕49% для 246 группы,
😕46,8% для 247 группы
😕и 47,4% для 248 группы.
Сколько пользователей в каждой экспериментальной группе?
all_info = filtered_df.groupby('exp_id', as_index=False).agg({'user_id':'nunique'})
all_info.columns=['exp_id', 'n_users']
print(all_info)
exp_id n_users 0 246 2484 1 247 2513 2 248 2537
Виктория: В данных по экспериментальным группам есть небольшая разница в количестве между группами.
Есть 2 контрольные группы для А/А-эксперимента, чтобы проверить корректность всех механизмов и расчётов. Проверьте, находят ли статистические критерии разницу между выборками 246 и 247.
Виктория: Ниже введна функция, которая принимает два датафрейма с логами и определенное событие. Затем она попарно сравнивает доли пользователей, совершивших данное событие в группе 1 и группе 2, и проверяет, есть ли статистически значимая разница между этими долями. Входные параметры функции включают df1 и df2 - датафреймы с логами, event - определенное событие, и alpha - критический уровень статистической значимости.
def z_test(df1, df2, event, alpha):
# число пользователей в группе 1 и группе 2:
n_users = np.array([df1['user_id'].nunique(), df2['user_id'].nunique()])
# число пользователей, совершивших событие в группе 1 и группе 2
success = np.array([df1[df1['event_name'] == event]['user_id'].nunique(),
df2[df2['event_name'] == event]['user_id'].nunique()])
#пропорция успехов в первой группе:
p1 = success[0]/n_users[0]
#пропорция успехов во второй группе:
p2 = success[1]/n_users[1]
#пропорция успехов в комбинированном датасете:
p_combined = (success[0] + success[1]) / (n_users[0] + n_users[1])
# разница пропорций в датасетах
difference = p1 - p2
#считаю статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/n_users[0] + 1/n_users[1]))
#задаю стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print(event)
print('p-значение: ', p_value)
if (p_value < alpha):
print("Отвергаем нулевую гипотезу: между долями есть значимая разница")
else:
print("Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными")
#составляю список всех возможных событий
event_list = filtered_df['event_name'].unique()
Виктория:
Нулевая гипотеза (H0): Разница между выборками 246 и 247 не является статистически значимой.
Альтернативная гипотеза (H1): Разница между выборками 246 и 247 статистически значима.
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 247:
alpha = 0.05 # критический уровень статистической значимости
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 246], filtered_df[filtered_df['exp_id'] == 247], event, alpha)
Tutorial p-значение: 0.9376996189257114 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Виктория: При заданном уровне значимости у нас нет оснований считать группы 246 и 247 разными. Не получилось отвергнуть нулевую гипотезу.
Виктория: 💭Вывод, что с помощью данного кода мы проверили, есть ли статистически значимая разница в количестве уникальных пользователей между группами 246 и 247. Нулевая гипотеза состоит в том, что разницы нет, а альтернативная гипотеза - что разница есть. При заданном уровне значимости мы не смогли отвергнуть нулевую гипотезу, что означает, что статистически значимой разницы в количестве уникальных пользователей между этими группами нет. Мы использовали статистический тест, который позволяет сравнить средние значения двух групп и определить, есть ли между ними статистически значимая разница. В данном случае мы использовали z-тест для независимых выборок. С помощью проверки гипотез для контрольных групп мы проверили, что разбиение по группам работает корректно - это главный момент.
Выберите самое популярное событие. Посчитайте число пользователей, совершивших это событие в каждой из контрольных групп. Посчитайте долю пользователей, совершивших это событие. Проверьте, будет ли отличие между группами статистически достоверным. Проделайте то же самое для всех других событий (удобно обернуть проверку в отдельную функцию). Можно ли сказать, что разбиение на группы работает корректно?
#подсчитываю количества уникальных пользователей для каждого события
unique_users = filtered_df.groupby('event_name')['user_id'].nunique().reset_index()
#нахожу события с наибольшим количеством уникальных пользователей
most_popular_event = unique_users.loc[unique_users['user_id'].idxmax()]
print(f"Самое популярное событие: {most_popular_event['event_name']}")
print(f"Количество уникальных пользователей: {most_popular_event['user_id']}")
Самое популярное событие: MainScreenAppear Количество уникальных пользователей: 7419
group_events = filtered_df.query('event_name == "MainScreenAppear"') \
.groupby('exp_id', as_index=False) \
.agg({'user_id':'nunique'})
group_events.rename(columns={'user_id':'exp_n_users'}, inplace=True)
group_events = group_events.merge(all_info, on='exp_id')
group_events['share_%'] = round(group_events['exp_n_users'] / group_events['n_users'], 2)
group_events
| exp_id | exp_n_users | n_users | share_% | |
|---|---|---|---|---|
| 0 | 246 | 2450 | 2484 | 0.99 |
| 1 | 247 | 2476 | 2513 | 0.99 |
| 2 | 248 | 2493 | 2537 | 0.98 |
Аналогично поступите с группой с изменённым шрифтом. Сравните результаты с каждой из контрольных групп в отдельности по каждому событию. Сравните результаты с объединённой контрольной группой. Какие выводы из эксперимента можно сделать?
Виктория:
Нулевая гипотеза (H0): Разница между выборками 246 и 248 не является статистически значимой.
Альтернативная гипотеза (H1): Разница между выборками 246 и 248 статистически значима.
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 248:
alpha = 0.05 # критический уровень статистической значимости
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 246], filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.8264294010087645 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Виктория: Значимой разницы между контрольной группой 246 и экспериментальной группой 248 не выявлено. Не получилось отвергнуть нулевую гипотезу.
Виктория:
Нулевая гипотеза (H0): Разница между выборками 247 и 248 не является статистически значимой.
Альтернативная гипотеза (H1): Разница между выборками 247 и 248 статистически значима.
#проверяю, есть ли статистически значимая разница между контрольными группами 247 и 248:
alpha = 0.05 # критический уровень статистической значимости
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 247], filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.765323922474501 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Виктория: Значимой разницы между контрольной группой 247 и экспериментальной группой 248 также не выявлено. Не получилось отвергнуть нулевую гипотезу.
Виктория:
Нулевая гипотеза (H0): Разница между контрольными группами 246 и 247 и экспериментальной группой 248 не является статистически значимой.
Альтернативная гипотеза (H1): Разница между контрольными группами 246 и 247 и экспериментальной группой 248 статистически значима.
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 247 и экспериментальной группой 248:
alpha = 0.05 # критический уровень статистической значимости
for event in event_list:
z_test(pd.concat([filtered_df[filtered_df['exp_id'] == 246],
filtered_df[filtered_df['exp_id'] == 247]]),
filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.764862472531507 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Виктория: Значимой разницы между контрольной группой 247 и экспериментальной группой 248 также не выявлено. Не получилось отвергнуть нулевую гипотезу.
Виктория: Таким образом, после проведения анализа результатов стало ясно, что изменение шрифтов в приложении не оказало заметного влияния на поведение пользователей. Сравнение результатов с контрольной группой также не показало статистически значимой разницы в долях пользователей, совершивших целевое действие. Это означает, что нет оснований полагать, что изменение шрифтов имеет какой-либо эффект на поведение пользователей.
Какой уровень значимости вы выбрали при проверке статистических гипотез выше? Посчитайте, сколько проверок статистических гипотез вы сделали. При уровне значимости 0.1 в 10% случаев можно ошибочно отклонить нулевую гипотезу при условии, что она верна. Какой уровень значимости стоит применить? Если вы хотите изменить его, проделайте предыдущие пункты и проверьте свои выводы.
Виктория: Необходимо посчитать, сколько проверок статистических гипотез было проведено. Здесь важно понять то, сколько проверок стат. тестов мы проводим, то есть сколько гипотез мы проверяем. Было 3 А/В теста и 1 А/А тест, в каждом мы проходились по 5 этапам, соответственно, было проведено 20 проверок гипотез на одних и тех же данных
В нашем случае, необходимо скорректировать уровень значимости для снижения вероятности ложнопозитивного результата при множественном тестировании гипотез, для этого следует использовать поправку уровня значимости. Не стоит применять поправку Бонферрони, поскольку в таком случае мы сильно уменьшаем мощность теста. Далее в статье про множественные поправки гипотез приводится формула, по которой можно расчитать уровень значимости с учетом поправки методом Шидака.
alpha = 0.05 #cтатистическая значимость в тестах
n = 20 #количество проверок статистических гипотез
#Метод Шидака, уровень значимости для n гипотез
adjusted_alpha = 1 - (1 - alpha) ** (1/n)
print(f"Скорректированный уровень значимости составляет примерно {adjusted_alpha:.5f}")
Скорректированный уровень значимости составляет примерно 0.00256
Виктория: 💭Скорректированный уровень значимости по методу Шидака является 0.00256. Перепроверяю гипотезы с откорректированными данными alpha.
Виктория:
Нулевая гипотеза (H0): Доли выборок равны.
Альтернативная гипотеза (H1): Доли выборок различаются.
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 247:
alpha = 0.00256 # cкорректированный уровень статистической значимости по методу Шидака
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 246], filtered_df[filtered_df['exp_id'] == 247], event, alpha)
Tutorial p-значение: 0.9376996189257114 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.7570597232046099 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.2480954578522181 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.22883372237997213 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.11456679313141849 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 248:
alpha = 0.00256 # cкорректированный уровень статистической значимости по методу Шидака
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 246], filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.8264294010087645 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.2949721933554552 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.20836205402738917 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.07842923237520116 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.2122553275697796 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#проверяю, есть ли статистически значимая разница между контрольными группами 247 и 248:
alpha = 0.00256 # cкорректированный уровень статистической значимости по методу Шидака
for event in event_list:
z_test(filtered_df[filtered_df['exp_id'] == 247], filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.765323922474501 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.4587053616621515 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.9197817830592261 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.5786197879539783 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.7373415053803964 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
#проверяю, есть ли статистически значимая разница между контрольными группами 246 и 247 и экспериментальной группой 248:
alpha = 0.00256 # cкорректированный уровень статистической значимости по методу Шидака
for event in event_list:
z_test(pd.concat([filtered_df[filtered_df['exp_id'] == 246],
filtered_df[filtered_df['exp_id'] == 247]]),
filtered_df[filtered_df['exp_id'] == 248], event, alpha)
Tutorial p-значение: 0.764862472531507 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными MainScreenAppear p-значение: 0.29424526837179577 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными OffersScreenAppear p-значение: 0.43425549655188256 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными CartScreenAppear p-значение: 0.18175875284404386 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными PaymentScreenSuccessful p-значение: 0.6004294282308704 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Виктория: В результате нашего анализа, проведенного с использованием метода Шидака для коррекции уровня значимости, мы пришли к выводу, что нет статистически значимых различий в долях между группами во всех проведенных A/B и A/A тестах. Это означает, что мы не можем отвергнуть нулевую гипотезу о равенстве долей. Таким образом, наши результаты не подтверждают наличие значимых различий между группами (контрольные группы 246 с 247 и экспериментальная 248) по пяти параметрам (события: Tutorial, MainScreenAppear, OffersScreenAppear, CartScreenAppear, PaymentScreenSuccessful.
Виктория: Вывод:
Частота событий в логе:
MainScreenAppear (Главный экран) - 117328 раз
OffersScreenAppear (Продающий экран) - 46333 раз
CartScreenAppear (Корзина) - 42303 раз
PaymentScreenSuccessful (Завершение оплаты) - 33918 раз
Tutorial (Урок) - 1005 раз
Мы выяснили, что большинство пользователей (98.47%) посещают главную страницу приложения, а только 60,96% пользователей просматривают страницу с каталогом товаров. Скорее всего, это может быть связано с проблемами в работе приложения или другими факторами, которые следует исследовать. Нужно понять, почему мы "теряем" 37,51% пользователей на данном этапе.
Анализ воронки событий показал, что только 46,97% пользователей доходят до завершения оплаты.
Мы провели А/А/В-эксперимент, в котором изменили шрифты в приложении для группы В. Однако, результаты эксперимента не показали статистически значимой разницы между контрольными группами и группой с измененными шрифтами. Это означает, что изменение шрифтов не оказало значимого влияния на поведение пользователей.
Исходя из результатов эксперимента, мы можем заключить, что изменение шрифтов не является необходимым для улучшения пользовательского опыта. Однако, следует продолжить исследования и анализировать другие факторы, которые могут влиять на поведение пользователей в приложении.
Количество уникальных пользователей по группам:
Контрольная группа 246 - 2484 пользователей
Контрольная группа 247 - 2513 пользователей
Экспериментальная группа 248 - 2537 пользователей
В будущем стоит работать над улучшением методологии проведения А/А/В-экспериментов, чтобы размеры тестовых групп были одинаковыми и результаты были более точными и надежными.